package mailer

import (
	"bytes"
	"crypto/tls"
	"encoding/base64"
	"errors"
	"fmt"
	"io/ioutil"
	"mime/quotedprintable"
	"net"
	"net/smtp"
	"path"
	"strconv"
	"strings"
	"time"
)

type Mailer struct {
	Host     string
	Port     int
	UserName string
	Password string
	OpenTsl  bool
}

// MiniType类型枚举
var miniTypeMap = map[string]string{
	".jpg":  "image/jpeg",
	".jpeg": "image/jpeg",
	".png":  "image/png",
	".gif":  "image/gif",
	".bmp":  "application/x-bmp",
}

type attachment struct {
	Name      string
	MiniType  string
	ContentId string
	Content   string
}

type SendMail struct {
	option      Mailer   // 邮箱配置信息
	from        string   // 发送人名称 默认是发送邮箱
	to          []string // 收件人
	cc          []string // 抄送人
	bcc         []string // 密送人
	subject     string   // 邮件标题
	body        string   // 邮件内容
	bodyType    string   // 邮件正文类下 html/plain
	attachments []attachment
}

// SetType 设置邮件类型 html/plain
func (sm *SendMail) SetType(bodyType string) *SendMail {
	sm.bodyType = bodyType
	return sm
}

// SetHtml 设置html格式邮件类型
func (sm *SendMail) SetHtml() *SendMail {
	return sm.SetType("html")
}

// SetText 设置文本邮件类型
func (sm *SendMail) SetText() *SendMail {
	return sm.SetType("plain")
}

// From 添加发送人名称
func (sm *SendMail) From(fromuser string) *SendMail {
	sm.from = fromuser
	return sm
}

// To 添加收件人
func (sm *SendMail) To(touser []string) *SendMail {
	sm.to = touser
	return sm
}

// Cc 添加抄送人
func (sm *SendMail) Cc(ccuser []string) *SendMail {
	sm.cc = ccuser
	return sm
}

// Bcc 添加密送人
func (sm *SendMail) Bcc(bccuser []string) *SendMail {
	sm.bcc = bccuser
	return sm
}

// Subject添加邮件主题
func (sm *SendMail) Subject(subject string) *SendMail {
	sm.subject = subject
	return sm
}

// byte2Base64将byte数据转base64
// rfc2045中要求base64一行不要超过76个字符，超过需要添加换行符 https://tools.ietf.org/html/rfc2045
func (sm *SendMail) byte2Base64(strByte []byte) string {
	strByteLen := len(strByte)

	payload := make([]byte, base64.StdEncoding.EncodedLen(strByteLen))
	base64.StdEncoding.Encode(payload, strByte)

	stream := ""
	for index, size := 0, len(payload); index < size; index++ {
		stream += string(payload[index])
		if (index+1)%76 == 0 {
			stream += "\r\n"
		}
	}
	return stream
}

// quotedEncode 将字符串进行quoted-printable编码
func (sm *SendMail) quotedEncode(str string) string {
	buffer := bytes.NewBuffer(nil)
	w := quotedprintable.NewWriter(buffer)
	w.Write([]byte(str))
	w.Close()
	return buffer.String()
}

// 添加附件
func (sm *SendMail) AddAttachment(filePath string) (contentId string, err error) {
	fileName := path.Base(filePath)
	fileExt := path.Ext(filePath)
	fileMiniType := "application/octet-stream"
	if _, ok := miniTypeMap[fileExt]; ok {
		fileMiniType = miniTypeMap[fileExt]
	}

	fileBytes, err := ioutil.ReadFile(filePath)
	if err != nil {
		return contentId, err
	}

	contentId = "qqivy" + strconv.Itoa(len(sm.attachments)+1) + "@orange.generate"
	contentString := sm.byte2Base64(fileBytes)

	attachmentInfo := attachment{
		Name:      fileName,
		MiniType:  fileMiniType,
		ContentId: contentId,
		Content:   contentString,
	}

	sm.attachments = append(sm.attachments, attachmentInfo)
	return contentId, nil
}

// 发送邮件
func (sm *SendMail) Send(message string) (err error) {
	err = sm.validata()
	if err != nil {
		return err
	}

	// Set up authentication information.
	auth := smtp.PlainAuth("", sm.option.UserName, sm.option.Password, sm.option.Host)
	buffer := bytes.NewBuffer(nil)
	boudary := "THIS_IS_BOUNDARY_ORANGE"
	sm.body = message

	sm.writeHeader(buffer, boudary)
	sm.writeBody(buffer, boudary)
	sm.writeAttachment(buffer, boudary)
	buffer.WriteString("\r\n--" + boudary + "--\r\n")

	toUsers := make([]string, len(sm.to)+len(sm.cc)+len(sm.bcc))
	toUsers = append(sm.to, sm.cc...)
	toUsers = append(toUsers, sm.bcc...)

	addr := fmt.Sprintf("%s:%d", sm.option.Host, sm.option.Port)

	if sm.option.OpenTsl == true {
		err = sm.option.sendMailByTslDo(addr, auth, sm.option.UserName, toUsers, buffer.Bytes())
	} else {
		err = smtp.SendMail(addr, auth, sm.option.UserName, toUsers, buffer.Bytes())
	}

	return err
}

// sendMailByTslDo
func (mail *Mailer) sendMailByTslDo(addr string, auth smtp.Auth, from string,
	to []string, msg []byte) (err error) {

	//create smtp client
	c, err := mail.TlsDial(addr)

	if err != nil {
		return err
	}
	defer c.Close()

	if auth != nil {
		if ok, _ := c.Extension("AUTH"); ok {
			if err = c.Auth(auth); err != nil {
				return err
			}
		}
	}

	if err = c.Mail(from); err != nil {
		return err
	}

	for _, addr := range to {
		if err = c.Rcpt(addr); err != nil {
			return err
		}
	}

	w, err := c.Data()
	if err != nil {
		return err
	}

	_, err = w.Write(msg)
	if err != nil {
		return err
	}

	err = w.Close()
	if err != nil {
		return err
	}

	return c.Quit()
}

//return a smtp client by TLS connect
func (mail *Mailer) TlsDial(addr string) (*smtp.Client, error) {
	conn, err := tls.Dial("tcp", addr, nil)
	if err != nil {
		return nil, err
	}
	//分解主机端口字符串
	host, _, _ := net.SplitHostPort(addr)
	return smtp.NewClient(conn, host)
}

// writeAttachment 构建smtp邮件附件
func (sm *SendMail) writeAttachment(buffer *bytes.Buffer, boudary string) (attachment string) {
	for _, item := range sm.attachments {
		attachment = "\r\n--" + boudary + "\r\n"
		attachment += "Content-Type: " + item.MiniType + "; name=" + item.Name + "\r\n"
		attachment += "Content-Transfer-Encoding: base64\r\n"
		attachment += "Content-ID: <" + item.ContentId + ">\r\n"
		if item.MiniType != "application/octet-stream" {
			attachment += "Content-Disposition: inline; filename=" + item.Name + "\r\n"
		}

		attachment += "\r\n"
		attachment += item.Content
		attachment += "\r\n"

		buffer.WriteString(attachment)
	}
	return attachment
}

// writeBody 构建smtp邮件正文
func (sm *SendMail) writeBody(buffer *bytes.Buffer, boudary string) string {
	body := "\r\n--" + boudary + "\r\n"
	body += "Content-Type: text/" + sm.bodyType + "; charset=utf-8\r\n"
	body += "Content-Transfer-Encoding: quoted-printable\r\n"
	body += "\r\n" + sm.quotedEncode(sm.body) + "\r\n\r\n"
	buffer.WriteString(body)

	return body
}

// writeHeader 构建smtp邮件头部
func (sm *SendMail) writeHeader(buffer *bytes.Buffer, boudary string) string {
	subject := "=?UTF-8?B?" + base64.StdEncoding.EncodeToString([]byte(sm.subject)) + "?="
	header := "Date: " + time.Now().Format(time.RFC1123Z) + "\r\n"
	header += "Subject: " + subject + "\r\n"
	header += fmt.Sprintf("From: %s <%s>\r\n", "=?utf-8?Q?"+sm.quotedEncode(sm.from)+"?=", sm.option.UserName)
	header += "To: " + strings.Join(sm.to, ", ") + "\r\n"
	if len(sm.cc) > 0 {
		header += "Cc: " + strings.Join(sm.cc, ", ") + "\r\n"
	}
	if len(sm.bcc) > 0 {
		header += "Bcc: " + strings.Join(sm.bcc, ", ") + "\r\n"
	}
	header += "MIME-Version: 1.0\r\n"
	header += "Content-Type: multipart/related;\r\n boundary=\"" + boudary + "\"\r\n"

	headerString := header
	headerString += "\r\n"
	buffer.WriteString(headerString)
	return headerString
}

// validata 检测发送信息有效性
func (sm *SendMail) validata() (err error) {
	if sm.subject == "" {
		return errors.New("mail subject is empty")
	}
	if len(sm.to) == 0 {
		return errors.New("to user addr is empty")
	}
	return
}

// 获取发送邮件对象
func GetSendMailer(mailOption Mailer) (ret *SendMail, err error) {
	if mailOption.Host == "" || mailOption.Port == 0 ||
		mailOption.UserName == "" || mailOption.Password == "" {
		return ret, errors.New("mail option have an error")
	}
	ret = &SendMail{
		option:   mailOption,
		from:     mailOption.UserName, // 配置默认发送人
		bodyType: "html",
	}

	// 465端口自动开启tsl
	if mailOption.Port == 465 {
		ret.option.OpenTsl = true
	}

	return ret, nil
}
